Passed
Push — master ( 3f23ea...c594c9 )
by Jan
05:15 queued 11s
created

AjaxUI.onPopState   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
dl 0
loc 12
rs 10
1
/*
2
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony)
3
 *
4
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU General Public License
8
 * as published by the Free Software Foundation; either version 2
9
 * of the License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
19
 *
20
 */
21
22
/**
23
 * Extract the title (The name between the <title> tags) of a HTML snippet.
24
 * @param {string} html The HTML code which should be searched.
25
 * @returns {string} The title extracted from the html.
26
 */
27
function extractTitle(html : string) : string {
28
    let title : string = "";
29
    let regex = /<title>(.*?)<\/title>/gi;
30
    if (regex.test(html)) {
31
        let matches = html.match(regex);
32
        for(let match in matches) {
33
            title = $(matches[match]).text();
34
        }
35
    }
36
    return title;
37
}
38
39
40
class AjaxUI {
41
42
    protected BASE = "/";
43
44
    private trees_filled : boolean = false;
45
46
    private statePopped : boolean = false;
47
48
    public xhr : XMLHttpRequest;
49
50
    public constructor()
51
    {
52
        //Make back in the browser go back in history
53
        window.onpopstate = this.onPopState;
54
        $(document).ajaxError(this.onAjaxError.bind(this));
55
        //$(document).ajaxComplete(this.onAjaxComplete.bind(this));
56
    }
57
58
    /**
59
     * Starts the ajax ui und execute handlers registered in addStartAction().
60
     * Should be called in a document.ready, after handlers are set.
61
     */
62
    public start(disabled : boolean = false)
63
    {
64
        if(disabled) {
65
            return;
66
        }
67
68
        console.info("AjaxUI started!");
69
70
        this.BASE = $("body").data("base-url");
71
        //If path doesn't end with slash, add it.
72
        if(this.BASE[this.BASE.length - 1] !== '/') {
73
            this.BASE = this.BASE + '/';
74
        }
75
        console.info("Base path is " + this.BASE);
76
77
        //Show flash messages
78
        $(".toast").toast('show');
79
80
81
        /**
82
         * Save the XMLHttpRequest that jQuery used, to the class, so we can acess the responseURL property.
83
         * This is a work-around as long jQuery does not implement this property in its jQXHR objects.
84
         */
85
        //@ts-ignore
86
        jQuery.ajaxSettings.xhr = function () {
87
            //@ts-ignore
88
            let xhr = new window.XMLHttpRequest();
89
            //Save the XMLHttpRequest to the class.
90
            ajaxUI.xhr = xhr;
91
            return xhr;
92
        };
93
94
95
        this.registerLinks();
96
        this.registerForm();
97
        this.fillTrees();
98
99
        this.initDataTables();
100
101
        //Trigger start event
102
        $(document).trigger("ajaxUI:start");
103
    }
104
105
    /**
106
     * Fill the trees with the given data.
107
     */
108
    public fillTrees()
109
    {
110
        let categories =  localStorage.getItem("tree_datasource_tree-categories");
111
        let devices =  localStorage.getItem("tree_datasource_tree-devices");
112
        let tools =  localStorage.getItem("tree_datasource_tree-tools");
113
114
        if(categories == null) {
115
            categories = "categories";
116
        }
117
118
        if(devices == null) {
119
            devices = "devices";
120
        }
121
122
        if(tools == null) {
123
            tools = "tools";
124
        }
125
126
        this.treeLoadDataSource("tree-categories", categories);
127
        this.treeLoadDataSource("tree-devices", devices);
128
        this.treeLoadDataSource("tree-tools", tools);
129
130
        this.trees_filled = true;
131
132
        //Register tree btns to expand all, or to switch datasource.
133
        $(".tree-btns").click(function (event) {
134
            event.preventDefault();
135
            $(this).parents("div.dropdown").removeClass('show');
136
            //$(this).closest(".dropdown-menu").removeClass('show');
137
            $(".dropdown-menu.show").removeClass("show");
138
            let mode = $(this).data("mode");
139
            let target = $(this).data("target");
140
            let text = $(this).text() + " \n<span class='caret'></span>"; //Add caret or it will be removed, when written into title
141
142
            if (mode==="collapse") {
143
                // @ts-ignore
144
                $('#' + target).treeview('collapseAll', { silent: true });
145
            }
146
            else if(mode==="expand") {
147
                // @ts-ignore
148
                $('#' + target).treeview('expandAll', { silent: true });
149
            } else {
150
                localStorage.setItem("tree_datasource_" + target, mode);
151
                ajaxUI.treeLoadDataSource(target, mode);
152
            }
153
154
            return false;
155
        });
156
    }
157
158
    /**
159
     * Load the given url into the tree with the given id.
160
     * @param target_id
161
     * @param datasource
162
     */
163
    protected treeLoadDataSource(target_id, datasource) {
164
        let text : string = $(".tree-btns[data-mode='" + datasource + "']").html();
165
        text = text + " \n<span class='caret'></span>"; //Add caret or it will be removed, when written into title
166
        switch(datasource) {
167
            case "categories":
168
                ajaxUI.initTree("#" + target_id, 'tree/categories');
169
                break;
170
            case "locations":
171
                ajaxUI.initTree("#" + target_id, 'tree/locations');
172
                break;
173
            case "footprints":
174
                ajaxUI.initTree("#" + target_id, 'tree/footprints');
175
                break;
176
            case "manufacturers":
177
                ajaxUI.initTree("#" + target_id, 'tree/manufacturers');
178
                break;
179
            case "suppliers":
180
                ajaxUI.initTree("#" + target_id, 'tree/suppliers');
181
                break;
182
            case "tools":
183
                ajaxUI.initTree("#" + target_id, 'tree/tools');
184
                break;
185
            case "devices":
186
                ajaxUI.initTree("#" + target_id, 'tree/devices');
187
                break;
188
        }
189
190
        $( "#" + target_id + "-title").html(text);
191
    }
192
193
    /**
194
     * Fill a treeview with data from the given url.
195
     * @param tree The Jquery selector for the tree (e.g. "#tree-tools")
196
     * @param url The url from where the data should be loaded
197
     */
198
    public initTree(tree, url) {
199
        //let contextmenu_handler = this.onNodeContextmenu;
200
        $.getJSON(ajaxUI.BASE + url, function (data) {
201
            // @ts-ignore
202
            $(tree).treeview({
203
                data: data,
204
                enableLinks: false,
205
                showIcon: false,
206
                showBorder: true,
207
                searchResultBackColor: '#ffc107',
208
                searchResultColor: '#000',
209
                onNodeSelected: function(event, data) {
210
                    if(data.href) {
211
                        ajaxUI.navigateTo(data.href);
212
                    }
213
                },
214
                //onNodeContextmenu: contextmenu_handler,
215
                expandIcon: "fas fa-plus fa-fw fa-treeview", collapseIcon: "fas fa-minus fa-fw fa-treeview"})
216
                .on('initialized', function() {
217
                    $(this).treeview('collapseAll', { silent: true });
218
219
                    //Implement searching if needed.
220
                    if($(this).data('treeSearch')) {
221
                        let _this = this;
222
                        let $search = $($(this).data('treeSearch'));
223
                        $search.on( 'input', function() {
224
                            $(_this).treeview('collapseAll', { silent: true });
225
                            $(_this).treeview('search', [$search.val()]);
226
                        });
227
                    }
228
                });
229
        });
230
    }
231
232
233
    /**
234
     * Register all links, for loading via ajax.
235
     */
236
    public registerLinks()
237
    {
238
        // Unbind all old handlers, so the things are not executed multiple times.
239
        $('a').not(".link-external, [data-no-ajax], .page-link, [href^='javascript'], [href^='#']").unbind('click').click(function (event) {
240
                let a = $(this);
241
                let href = $.trim(a.attr("href"));
242
                //Ignore links without href attr and nav links ('they only have a #)
243
                if(href != null && href != "" && href.charAt(0) !== '#') {
244
                    event.preventDefault();
245
                    ajaxUI.navigateTo(href);
246
                }
247
            }
248
        );
249
        console.debug('Links registered!');
250
    }
251
252
    protected getFormOptions() : JQueryFormOptions
253
    {
254
        return  {
255
            success: this.onAjaxComplete,
256
            beforeSerialize: function($form, options) : boolean {
257
258
                //Update the content of textarea fields using CKEDITOR before submitting.
259
                //@ts-ignore
260
                if(typeof CKEDITOR !== 'undefined') {
261
                    //@ts-ignore
262
                    for (let name in CKEDITOR.instances) {
263
                        //@ts-ignore
264
                        CKEDITOR.instances[name].updateElement();
265
                    }
266
                }
267
268
                //Check every checkbox field, so that it will be submitted (only valid fields are submitted)
269
                $form.find("input[type=checkbox].tristate").prop('checked', true);
270
271
                return true;
272
            },
273
            beforeSubmit: function (arr, $form, options) : boolean {
274
                //When data-with-progbar is specified, then show progressbar.
275
                if($form.data("with-progbar") != undefined) {
276
                    ajaxUI.showProgressBar();
277
                }
278
                return true;
279
            }
280
        };
281
    }
282
283
    /**
284
     * Register all forms for loading via ajax.
285
     */
286
    public registerForm()
287
    {
288
289
        let options = this.getFormOptions();
290
291
        $('form').not('[data-no-ajax]').ajaxForm(options);
292
293
        console.debug('Forms registered!');
294
    }
295
296
297
    /**
298
     * Submits the given form via ajax.
299
     * @param form The form that will be submmitted.
300
     * @param btn The btn via which the form is submitted
301
     */
302
    public submitForm(form, btn = null)
303
    {
304
        let options = ajaxUI.getFormOptions();
305
306
        if(btn) {
307
            options.data = {};
308
            options.data[$(btn).attr('name')] = $(btn).attr('value');
309
        }
310
311
        $(form).ajaxSubmit(options);
312
    }
313
314
315
    /**
316
     * Show the progressbar
317
     */
318
    public showProgressBar()
319
    {
320
        //Blur content background
321
        $('#content').addClass('loading-content');
322
323
        // @ts-ignore
324
        $('#progressModal').modal({
325
            keyboard: false,
326
            backdrop: false,
327
            show: true
328
        });
329
    }
330
331
    /**
332
     * Hides the progressbar.
333
     */
334
    public hideProgressBar()
335
    {
336
        // @ts-ignore
337
        $('#progressModal').modal('hide');
338
        //Remove the remaining things of the modal
339
        $('.modal-backdrop').remove();
340
        $('body').removeClass('modal-open');
341
        $('body, .navbar').css('padding-right', "");
342
343
    }
344
345
346
    /**
347
     * Navigates to the given URL
348
     * @param url The url which should be opened.
349
     * @param show_loading Show the loading bar during loading.
350
     */
351
    public navigateTo(url : string, show_loading : boolean = true)
352
    {
353
        if(show_loading) {
354
            this.showProgressBar();
355
        }
356
        $.ajax(url, {
357
            success: this.onAjaxComplete
358
        });
359
        //$.ajax(url).promise().done(this.onAjaxComplete);
360
    }
361
362
    /**
363
     * Called when an error occurs on loading ajax. Outputs the message to the console.
364
     */
365
    private onAjaxError (event, request, settings) {
366
        'use strict';
367
        //Ignore aborted requests.
368
        if (request.statusText =='abort') {
369
            return;
370
        }
371
372
        //Ignore ajax errors with 200 code (like the ones during 2FA authentication)
373
        if(request.status == 200) {
374
            return;
375
        }
376
377
        console.error("Error getting the ajax data from server!");
378
        console.log(event);
379
        console.log(request);
380
        console.log(settings);
381
382
        ajaxUI.hideProgressBar();
383
384
        //Create error text
385
        let title = "";
386
387
        switch(request.status) {
388
            case 500:
389
                title =  'Internal Server Error!';
390
                break;
391
            case 404:
392
                title = "Site not found!";
393
                break;
394
            case 403:
395
                title = "Permission denied!";
396
                break;
397
        }
398
399
        var alert = bootbox.alert(
400
            {
401
                size: 'large',
402
                message: function() {
403
                    let msg = "Error getting data from Server! <b>Status Code: " + request.status + "</b>";
404
405
                    msg += '<br><br><a class=\"btn btn-link\" data-toggle=\"collapse\" href=\"#iframe_div\" >' + 'Show response' + "</a>";
406
                    msg += "<div class=\" collapse\" id='iframe_div'><iframe height='512' width='100%' id='iframe'></iframe></div>";
407
408
                    return msg;
409
                },
410
                title: title,
411
                callback: function () {
412
                    //Remove blur
413
                    $('#content').removeClass('loading-content');
414
                }
415
416
            });
417
418
        //@ts-ignore
419
        alert.init(function (){
420
            var dstFrame = document.getElementById('iframe');
421
            //@ts-ignore
422
            var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
423
            dstDoc.write(request.responseText);
424
            dstDoc.close();
425
        });
426
427
428
429
        //If it was a server error and response is not empty, show it to user.
430
        if(request.status == 500 && request.responseText !== "")
431
        {
432
            console.log("Response:" + request.responseText);
433
        }
434
    }
435
436
    /**
437
     * This function gets called every time, the "back" button in the browser is pressed.
438
     * We use it to load the content from history stack via ajax and to rewrite url, so we only have
439
     * to load #content-data
440
     * @param event
441
     */
442
    private onPopState(event)
443
    {
444
        let page : string = location.href;
445
        ajaxUI.statePopped = true;
446
        ajaxUI.navigateTo(page);
447
    }
448
449
    /**
450
     * This function takes the response of an ajax requests, and does the things we need to do for our AjaxUI.
451
     * This includes inserting the content and pushing history.
452
     * @param responseText
453
     * @param textStatus
454
     * @param jqXHR
455
     */
456
    private onAjaxComplete(responseText: string, textStatus: string, jqXHR: any)
457
    {
458
        console.debug("Ajax load completed!");
459
460
461
        ajaxUI.hideProgressBar();
462
463
        /* We need to do the url checking before the parseHTML, so that we dont get wrong url name, caused by scripts
464
           in the new content */
465
        // @ts-ignore
466
        let url = this.url;
467
        //Check if we were redirect to a new url, then we should use that as new url.
468
        if(ajaxUI.xhr.responseURL) {
469
            url = ajaxUI.xhr.responseURL;
470
        }
471
472
473
        //Parse response to DOM structure
474
        //We need to preserve javascript, so the table ca
475
        let dom = $.parseHTML(responseText, document, true);
476
        //And replace the content container
477
        $("#content").replaceWith($("#content", dom));
478
        //Replace login menu too (so everything is up to date)
479
        $("#login-content").replaceWith($('#login-content', dom));
480
481
        //Replace flash messages and show them
482
        $("#message-container").replaceWith($('#message-container', dom));
483
        $(".toast").toast('show');
484
485
        //Set new title
486
        let title  = extractTitle(responseText);
487
        document.title = title;
488
489
        //Push to history, if we currently arent poping an old value.
490
        if(!ajaxUI.statePopped) {
491
            history.pushState(null, title, url);
492
        } else {
493
            //Clear pop state
494
            ajaxUI.statePopped = false;
495
        }
496
497
        //Do things on the new dom
498
        ajaxUI.registerLinks();
499
        ajaxUI.registerForm();
500
        ajaxUI.initDataTables();
501
502
        //Trigger reload event
503
        $(document).trigger("ajaxUI:reload");
504
    }
505
506
    /**
507
     * Init all datatables marked with data-datatable based on their data-settings attribute.
508
     */
509
    protected initDataTables()
510
    {
511
        //@ts-ignore
512
        $($.fn.DataTable.tables()).DataTable().fixedHeader.disable();
513
        //@ts-ignore
514
        $($.fn.DataTable.tables()).DataTable().destroy();
515
516
        //Find all datatables and init it.
517
        let $tables = $('[data-datatable]');
518
        $.each($tables, function(index, table) {
519
            let $table = $(table);
520
            let settings = $table.data('settings');
521
522
            //@ts-ignore
523
            var promise = $('#part_list').initDataTables(settings,
524
                {
525
                    colReorder: true,
526
                    responsive: true,
527
                    "fixedHeader": { header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones.
528
                        headerOffset: $("#navbar").height()},
529
                    "buttons": [ {
530
                        "extend": 'colvis',
531
                        'className': 'mr-2 btn-light',
532
                        "text": "<i class='fa fa-cog'></i>"
533
                    }],
534
                    "rowCallback": function( row, data, index ) {
535
                        //Check if we have a level, then change color of this row
536
                        if (data.level) {
537
                            let style = "";
538
                            switch(data.level) {
539
                                case "emergency":
540
                                case "alert":
541
                                case "critical":
542
                                case "error":
543
                                    style = "table-danger";
544
                                    break;
545
                                case "warning":
546
                                    style = "table-warning";
547
                                    break;
548
                                case "notice":
549
                                    style = "table-info";
550
                                    break;
551
                            }
552
553
                            if (style){
554
                                $(row).addClass(style);
555
                            }
556
                        }
557
                    }
558
                });
559
560
            //Register links.
561
            promise.then(function() {
562
                ajaxUI.registerLinks();
563
564
                //Set the correct title in the table.
565
                let title = $('#part-card-header-src');
566
                $('#part-card-header').html(title.html());
567
                $(document).trigger('ajaxUI:dt_loaded');
568
569
                //Attach event listener to update links after new page selection:
570
                $('#dt').on('draw.dt column-visibility.dt', function() {
571
                    ajaxUI.registerLinks();
572
                    $(document).trigger('ajaxUI:dt_loaded');
573
                });
574
            });
575
        });
576
577
        console.debug('Datatables inited.');
578
    }
579
}
580
581
export let ajaxUI = new AjaxUI();